package com.tevintao.marketingsystem.xposted;

import android.app.PendingIntent;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.RemoteException;

import com.tevintao.marketingsystem.log.LogManager;
import com.tevintao.marketingsystem.services.MockServices;

import java.lang.reflect.Method;
import java.util.Map;
import java.util.Set;
import java.util.WeakHashMap;

import de.robv.android.xposed.XC_MethodHook;
import de.robv.android.xposed.XposedBridge;


/**
 * 切记，一定要关闭  WifiManager  getScanResults
 */
public class HookLocation {
    private static HookLocation ourInstance = new HookLocation();

    public static HookLocation getInstance() {
        return ourInstance;
    }


    private static Thread mWorker = null;
    private static Handler mHandler = null;


    private HookLocation() {
        // Start a worker thread
        mWorker = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Looper.prepare();
                    mHandler = new Handler();
                    Looper.loop();
                } catch (Throwable ex) {
                    LogManager.getInstance().logHookError("Start a worker thread " + ex);
                }
            }
        });
        mWorker.start();
    }

    public void hookLocation() {
        HookDeviceModule.hookAllMethodByName(LocationManager.class, "removeUpdates", new LocationXcHook("removeUpdates"));
        HookDeviceModule.hookAllMethodByName(LocationManager.class, "requestLocationUpdates", new LocationXcHook("requestLocationUpdates"));
        HookDeviceModule.hookAllMethodByName(LocationManager.class, "requestSingleUpdate", new LocationXcHook("requestSingleUpdate"));


        HookDeviceModule.hookAllMethodByName(LocationManager.class, "getLastKnownLocation", new LocationXcHook("getLastKnownLocation"));
        HookDeviceModule.hookAllMethodByName(LocationManager.class, "getLastLocation", new LocationXcHook("getLastLocation"));
    }


    public void hookAllMethod() {
        Class lcl = LocationManager.class;

        Method[] methods = lcl.getDeclaredMethods();

        for (Method method : methods) {
            XposedBridge.hookMethod(method, new XC_MethodHook() {
                @Override
                protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
                    LogManager.getInstance().logInfo("method = " + param.method);
                    MockServices.getClient().appAccessPath("" + param.method);
                }
            });
        }

    }

    private class LocationXcHook extends XC_MethodHook {
        private String mMethodName = null;

        public LocationXcHook(String methodName) {
            this.mMethodName = methodName;
        }

        @Override
        protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
            LogManager.getInstance().logInfo("hookLocation " + mMethodName);
            if ("removeUpdates".equals(mMethodName)) {
                unProxyLocationListener(param, 0);
            } else if ("requestLocationUpdates".equals(mMethodName)) {
//                LogManager.getInstance().logInfo("requestLocationUpdates " + HookDeviceModule.getArgsStr(param.args));
                if (param.args.length > 0 && param.args[0] instanceof String) {
                    proxyLocationListener(param, 3);
                } else {
                    proxyLocationListener(param, 3);
                }
            } else if ("requestSingleUpdate".equals(mMethodName)) {
//                LogManager.getInstance().logInfo("requestSingleUpdate " + HookDeviceModule.getArgsStr(param.args));
                if (param.args.length > 0 && param.args[0] instanceof String) {
                    proxyLocationListener(param, 1);
                } else {
                    proxyLocationListener(param, 1);
                }
            }

        }

        @Override
        protected void afterHookedMethod(MethodHookParam param) throws Throwable {
            if ("getLastKnownLocation".equals(mMethodName)) {
                if (param.args.length > 0 && param.getResult() instanceof Location) {
                    Location location = (Location) param.getResult();
                    param.setResult(getDefaultDefacedLocation(location));
                }
            } else if ("getLastLocation".equals(mMethodName)) {
                if (param.getResult() instanceof Location) {
                    Location location = (Location) param.getResult();
                    param.setResult(getDefaultDefacedLocation(location));
                }
            }
        }


    }


    private void unProxyLocationListener(XC_MethodHook.MethodHookParam param, int arg) {
        if (param.args.length > arg)
            if (param.args[arg] instanceof PendingIntent)
                param.setResult(null);

            else if (param.args[arg] != null) {
                Object key = param.args[arg];
                synchronized (mMapProxy) {
                    if (mMapProxy.containsKey(key)) {
                        LogManager.getInstance().logInfo("Removing proxy uid=");
                        param.args[arg] = mMapProxy.get(key);
                        mMapProxy.remove(key);
                    }
                }
            }
    }

    private final Map<Object, Object> mMapProxy = new WeakHashMap<>();

    private void proxyLocationListener(XC_MethodHook.MethodHookParam param, int arg) throws Throwable {
        if (param.args.length > arg)
            if (param.args[arg] instanceof PendingIntent)
                param.setResult(null);

            else if (param.args[arg] != null && param.thisObject != null) {
                Object key = param.args[arg];
                synchronized (mMapProxy) {
                    // Reuse existing proxy
                    if (mMapProxy.containsKey(key)) {
                        LogManager.getInstance().logInfo("Reuse existing proxy uid=");
                        param.args[arg] = mMapProxy.get(key);
                        return;
                    }

                    // Already proxied
                    if (mMapProxy.containsValue(key)) {
                        LogManager.getInstance().logInfo("Already proxied uid=");
                        return;
                    }
                }

                // Create proxy
                LogManager.getInstance().logInfo("Creating proxy uid=");
                Object proxy = new ProxyLocationListener((LocationListener) param.args[arg]);

                // Use proxy
                synchronized (mMapProxy) {
                    mMapProxy.put(key, proxy);
                    mHandler.postDelayed(mSendLocation, 100);
                }
                param.args[arg] = proxy;
            }
    }


    private Runnable mSendLocation = new Runnable() {
        @Override
        public void run() {
            mHandler.removeCallbacks(mSendLocation);
            changeLocation();
            mHandler.postDelayed(mSendLocation, 1000);
        }
    };

    private void changeLocation() {
        Set<Object> objects = mMapProxy.keySet();
        for (Object o : objects) {
//                LogManager.getInstance().logInfo("o = " + o.toString());
            if (o instanceof LocationListener) {
                LocationListener current = (LocationListener) o;
                current.onLocationChanged(getDefaultDefacedLocation(null));
            }
        }
    }


    private class ProxyLocationListener implements LocationListener {
        private LocationListener mListener;

        public ProxyLocationListener(LocationListener listener) {
            mListener = listener;
        }

        @Override
        public void onLocationChanged(Location location) {
            StringBuffer sb = new StringBuffer();
            sb.append("source " + location.getLatitude() + ":" + location.getLongitude());
            Location fakeLocation = getDefaultDefacedLocation(location);
            sb.append("fakeLocation " + fakeLocation.getLatitude() + ":" + fakeLocation.getLongitude());
//            LogManager.getInstance().logInfo(sb.toString());
//            mListener.onLocationChanged(fakeLocation);
            LogManager.getInstance().logInfo("onLocationChanged --- from services " + sb.toString());
            changeLocation();
            mHandler.removeCallbacks(mSendLocation);
            mHandler.postDelayed(mSendLocation, 1500);
        }

        @Override
        public void onProviderDisabled(String provider) {
            mListener.onProviderDisabled(provider);
        }

        @Override
        public void onProviderEnabled(String provider) {
            mListener.onProviderEnabled(provider);
        }

        @Override
        public void onStatusChanged(String provider, int status, Bundle extras) {
            mListener.onStatusChanged(provider, status, extras);
        }


    }

    private static final float DEFAULT_ACCURACY = 5.0f;
    private static final float DEFAULT_BEARING = 0.0f;
    private static final float DEFAULT_SPEED = 10f;
    private static final double DEFAULT_ALTITUDE = 0.0d; // 海拔

    public static Location getDefaultDefacedLocation(Location location) {
        if (MockServices.checkClient()) {
            try {
                return MockServices.getClient().getLocation(location);
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        }


        String sLat = "0";
        String sLon = "0";
        String sAlt = "0";


        if (location == null) {
            location = new Location(LocationManager.GPS_PROVIDER);
            location.setAltitude(DEFAULT_ALTITUDE);
            location.setAccuracy(DEFAULT_ACCURACY);
            location.setBearing(DEFAULT_BEARING);
            location.setSpeed(DEFAULT_SPEED);
            location.setTime(System.currentTimeMillis());
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
                location.setElapsedRealtimeNanos(System.currentTimeMillis());
            }
        }
        location.setLatitude(Float.parseFloat(sLat) + (Math.random() * 2.0 - 1.0) * location.getAccuracy() * 9e-6);
        location.setLongitude(Float.parseFloat(sLon) + (Math.random() * 2.0 - 1.0) * location.getAccuracy() * 9e-6);
        location.setAltitude(Float.parseFloat(sAlt) + (Math.random() * 2.0 - 1.0) * location.getAccuracy());

        return location;
    }


}
